const path = require('path');
const fs = require('fs');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssPlugin = require('mini-css-extract-plugin');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const autoprefixer = require('autoprefixer');
const webpack = require('webpack');

const NODE_ENV = process.env.NODE_ENV || 'development';
const PUBLIC_URL = process.env.PUBLIC_URL || '/';
const WORK_DIR = path.resolve(__dirname);
const SRC_DIR = path.join(WORK_DIR, './src');

/**
 * 插入环境变量，可以在js代码中使用这些变量
 * 各个模式下会默认提供process.env.NODE_ENV
 * 此处暂且放着 根据以后开发情况酌情添加
 * @type {{'process.env': string}}
 */
const clientEnvironment = {
    'process.env': JSON.stringify({
        NODE_ENV,
        PUBLIC_URL,
    }),
};

// less/sass/css loader 公用部分
const getStyleLoaders = (cssOptions, preProcessor) => {
    const loaders = [
        // css样式抽离成单独文件
        MiniCssPlugin.loader,
        // 解析css
        {
            loader: require.resolve('css-loader'),
            options: cssOptions,
        },
        // postcss 补全前缀 flex
        {
            loader: require.resolve('postcss-loader'),
            options: {
                ident: 'postcss',
                plugins: () => [
                    require('postcss-flexbugs-fixes'),
                    autoprefixer({
                        flexbox: 'no-2009',
                    }),
                ],
            },
        },
    ];
    if (preProcessor) {
        loaders.push(
            typeof preProcessor === 'string' ?
                require.resolve(preProcessor) :
                preProcessor,
        );
    }
    return loaders;
};

// /////////////////////////////////////////
// Rule 配置对象   option.module.rules[Rule]
// /////////////////////////////////////////

// ESLint 在 Babel 转译前执行
const esLintRule = {
    test: /\.(js|jsx|mjs)$/,
    include: SRC_DIR,
    // 指定 loader 种类
    enforce: 'pre',
    use: [
        {
            options: {
                eslintPath: require.resolve('eslint'),
                baseConfig: {
                    extends: [],
                },
                ignore: false,
                useEslintrc: true,
            },
            loader: require.resolve('eslint-loader'),
        },
    ],
};
// Babel 转译; .mjs表示es6模块 import/export
const babelRule = {
    test: /\.(js|jsx|mjs)$/,
    include: SRC_DIR,
    use: [
        {
            loader: require.resolve('babel-loader'),
            options: {
                babelrc: false,
                presets: [require.resolve('babel-preset-react-app')],
                cacheDirectory: true,
                plugins: [
                    // `style: true` 会加载 less 文件 `css` 加载css文件
                    ['import', {libraryName: 'antd', libraryDirectory: 'es', style: true}],
                ],
            },
        },
    ],
};
// Css-Loader 处理工作目录下的css文件 开启css-module
const cssRuleWithModule = {
    test: /\.css$/,
    include: SRC_DIR,
    exclude: [/\/common\/.+/],
    use: getStyleLoaders({
        modules: true,
        importLoaders: 1,
    }),
};
// Css-Loader 处理工作目录下的css文件 关闭css-module
const cssRuleWithoutModule = {
    test: /\.css$/,
    include: [/\/common\/.+/],
    use: getStyleLoaders({
        modules: false,
    }),
};
// Less-Loader 预处理 src 目录的less文件 开启css-module
const lessRuleWithModule = {
    test: /\.less$/,
    exclude: [/[/\\\\]node_modules[/\\\\]/],
    use: getStyleLoaders({
        modules: true,
        importLoaders: 2,
    }, 'less-loader'),
};
// Less-Loader 处理第三方 less文件 关闭css-module
const lessRuleWithoutModule = {
    test: /\.less/,
    include: [/[/\\\\]node_modules[/\\\\]/],
    use: getStyleLoaders({
        modules: false,
        importLoaders: 1,
    }, {
        loader: require.resolve('less-loader'),
        // less3.0以上
        options: {javascriptEnabled: true},
    }),
};
// Url-Loader 将文件转换为 DataURL
const urlRule = {
    test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
    loader: require.resolve('url-loader'),
    options: {
        limit: 10000,
        // 按照开发目录生成目录结构
        name: '[path][name].[hash:8].[ext]',
    },
};
// File-loader 将require的文件导出在指定目录下 并 返回获取该文件的路由
const fileRule = {
    exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
    loader: require.resolve('file-loader'),
    options: {
        // 按照开发目录生成目录结构
        name: '[path][name].[hash:8].[ext]',
    },
};


// webpack 主配置
const config = {
    // 入口文件和loader从哪个目录开始查找 默认当前目录
    // context: path.resolve(__dirname),
    /**
     * 打包模式，development|production 会配置默认plugin
     * 提供全局变量 process.env.NODE_ENV 为 development
     * 启用 NamedChunksPlugin 和 NamedModulesPlugin
     */
    mode: 'production',

    /**
     * 数据类型
     * {
     *  string |
     *  [string] |
     *  object { <key>: string | [string] }|
     *  ()=>string | [string] | object { <key>: string | [string] }
     *  Promise.resolve(string | [string] | object { <key>: string | [string] })
     * }
     *
     * 传入字符串或数组（string|[string]）chunk 命名为main.js
     * 传入对象，则 key 为 chunk 名
     */
    entry: './src/index.js',

    // 描述 chunk 的存放信息
    output: {
        // chunk输出目录
        path: path.join(__dirname, './dist'),

        /**
         * 服务从哪个路由开始，输出的 bundle 路由从该路由开始 （[publicPath]/[name].bundle.js）；
         * 若不指定则是相对路径 ([name].bundle.js 例如：script src='0.bundle.js')
         */
        publicPath: PUBLIC_URL,

        // 开发环境使用 在文件中添加包含模块的相关信息
        pathinfo: false,

        /**
         * chunk 名称 单入口时可以固定文件名，默认为 main.js
         * 多入口时用占位符方式保证唯一，
         * 占位符：
         *      name: 入口文件名
         *      id: 内部使用的 chunk id
         *      hash: 每次构建的唯一 hash
         *      chunkhash: 基于 chunk 内容计算的 hash
         */
        filename: '[name].[chunkhash:8].js',

        // 非入口 chunk 名称
        chunkFilename: '[name].[chunkhash:8].bundle.js',
    },

    // 设置模块如何被解析 require.resolve的强化
    resolve: {
        // 路径别名 方便从子业务调用公用组件 避免 '../../../components/'
        alias: {
            common: path.join(__dirname, './src/common'),
            components: path.join(__dirname, './src/components'),
            containers: path.join(__dirname, './src/containers'),
        },
        /**
         * 解析目录时要使用的文件名
         * package.json 找不到 resolve.mainFields 字段，默认将 index.js 作为入口文件
         */
        mainFiles: ['index'],
        // package.json 表示包入口的字段 默认如下 表示浏览器环境
        mainFields: ['browser', 'module', 'main'],
        // 缺省后缀后 require.resolve 自动添加后缀尝试查找文件
        extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
    },

    // 这些选项决定了如何处理项目中的不同类型的模块
    module: {
        // 每个模块是否必须有 exports
        strictExportPresence: true,
        /**
         * 创建模块时，匹配请求的规则数组。这些规则能够修改模块的创建方式。
         * 能够对模块(module)应用 loader，或者修改解析器(parser)。
         *
         * 每个rule的参数可分为三类：
         *  1. 条件：test, include, exclude 和 resource
         *  2. 结果：loader, options, use
         *  3. 嵌套规则：oneof, rules
         */
        rules: [
            // 禁用 require.ensure
            {parser: {requireEnsure: false}},
            esLintRule,
            // 开发环境尽量使用IDE lint 加快实时编译速度  测试环境打包使用 esLintRule,
            {
                oneOf: [
                    babelRule,
                    cssRuleWithoutModule,
                    cssRuleWithModule,
                    lessRuleWithoutModule,
                    lessRuleWithModule,
                    urlRule,
                    fileRule,
                ],
            },
        ],
    },

    optimization: {
        splitChunks: {},
        // webpack bootstrap单独一个文件
        runtimeChunk: true,
        minimizer: [
            new UglifyJsPlugin({
                cache: true,
                parallel: true,
                sourceMap: true,
            }),
            // 压缩 css
            new OptimizeCSSAssetsPlugin({}),
        ],
    },
    plugins: [
        // 每次打包先清空 原先打包文件
        new CleanWebpackPlugin(['dist']),
        // public 目录下的文件拷贝到打包目录 排除index.html
        new CopyWebpackPlugin([{
            from: path.join(WORK_DIR, 'public'),
            to: path.join(WORK_DIR, 'dist'),
        }], {
            ignore: ['index.html'],
        }),
        new MiniCssPlugin({
            filename: '[name].[chunkhash:8].css',
            chunkFilename: '[id].[chunkhash:8].css',
        }),
        // 根据模板生成 HTML
        new HtmlWebpackPlugin({
            inject: true,
            template: path.resolve(WORK_DIR, './index.html'),
        }),
        // DllPlugin/DllReferencePlugin
        ...[
            require('./public/react.manifest'),
            require('./public/redux.manifest'),
        ].map(item => new webpack.DllReferencePlugin({
            context: WORK_DIR, // 与DllPlugin中的那个context保持一致
            /**
             * 这个字段对应webpack.dll.config.js中生成的那个json文件的路径
             * webpack打包时，会检测此文件中的映射，不会把存在映射的包打包进bundle.js
             */
            manifest: item,
        })),
        new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
    ],
    // js source map
    devtool: 'source-map',
    /**
     * 是否 polyfill 或 mock 某些 Node.js 全局变量和模块
     * 这里提供空对象 因为web环境不会用到
     */
    node: {
        console: false, // 浏览器环境已经存在
        dgram: 'empty',
        fs: 'empty',
        net: 'empty',
        tls: 'empty',
        child_process: 'empty',
    },
    performance: {
        // 控制webpack性能提示 false | "error" | "warning"
        hints: false,
    },
};


module.exports = config;
